Java 26-Day Course - Day 10: Access Modifiers and Encapsulation

Day 10: Access Modifiers and Encapsulation

Encapsulation is the principle of protecting an object’s internal data from the outside and allowing access only through designated methods. It’s like operating a car through the steering wheel and pedals instead of directly touching the engine. Java implements this through access modifiers.

The Four Access Modifiers

Let’s understand the scope of each access modifier.

// File: AccessModifierDemo.java
public class AccessModifierDemo {
    public String publicField = "Accessible from anywhere";
    protected String protectedField = "Same package + subclasses";
    String defaultField = "Same package only"; // package-private
    private String privateField = "This class only";

    public void publicMethod() {
        System.out.println("public method");
        System.out.println(privateField); // Accessible from within the class
    }

    protected void protectedMethod() {
        System.out.println("protected method");
    }

    void defaultMethod() {
        System.out.println("default method");
    }

    private void privateMethod() {
        System.out.println("private method");
    }

    // Access scope summary:
    // public    > protected > default > private
    // Anywhere    Same pkg+child  Same pkg    Same class

    public static void main(String[] args) {
        AccessModifierDemo demo = new AccessModifierDemo();
        demo.publicMethod();    // OK
        demo.protectedMethod(); // OK (same class)
        demo.defaultMethod();   // OK (same class)
        demo.privateMethod();   // OK (same class)
    }
}

Encapsulation with Getters/Setters

Hide fields as private and provide controlled access through public methods.

public class User {
    private String name;
    private String email;
    private int age;
    private String password;

    public User(String name, String email, int age) {
        this.name = name;
        setEmail(email);
        setAge(age);
    }

    // Getter: retrieve value
    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public int getAge() {
        return age;
    }

    // Setter: set value (with validation)
    public void setName(String name) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty.");
        }
        this.name = name.trim();
    }

    public void setEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Please enter a valid email address.");
        }
        this.email = email;
    }

    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Age must be between 0 and 150.");
        }
        this.age = age;
    }

    public void setPassword(String password) {
        if (password.length() < 8) {
            throw new IllegalArgumentException("Password must be at least 8 characters.");
        }
        this.password = password;
    }

    // No getter for password (intentionally preventing external exposure)

    @Override
    public String toString() {
        return "User{name='" + name + "', email='" + email + "', age=" + age + "}";
    }

    public static void main(String[] args) {
        User user = new User("Alice", "alice@example.com", 25);
        System.out.println(user);

        user.setAge(30);
        System.out.println("Updated age: " + user.getAge());

        // user.age = -5; // Compile error! (private)
        // user.setAge(-5); // Runtime error! (validation)
    }
}

Immutable Class

A safe class whose state cannot be changed once created.

public final class Money {
    private final long amount;
    private final String currency;

    public Money(long amount, String currency) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount cannot be negative.");
        }
        this.amount = amount;
        this.currency = currency;
    }

    // Only getters (no setters)
    public long getAmount() {
        return amount;
    }

    public String getCurrency() {
        return currency;
    }

    // Return new objects instead of modifying state
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currencies do not match.");
        }
        return new Money(this.amount + other.amount, this.currency);
    }

    public Money subtract(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currencies do not match.");
        }
        return new Money(this.amount - other.amount, this.currency);
    }

    @Override
    public String toString() {
        return amount + " " + currency;
    }

    public static void main(String[] args) {
        Money price = new Money(50000, "KRW");
        Money tax = new Money(5000, "KRW");
        Money total = price.add(tax);

        System.out.println("Price: " + price);   // 50000 KRW
        System.out.println("Tax: " + tax);        // 5000 KRW
        System.out.println("Total: " + total);    // 55000 KRW
        // The price object remains unchanged (immutable)
    }
}

Record Classes (Java 16+)

A concise syntax for creating immutable data classes.

// record: automatically generates constructor, getters, equals, hashCode, toString
public record Product(String name, int price, String category) {

    // Compact constructor: validation
    public Product {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Product name is required.");
        }
        if (price < 0) {
            throw new IllegalArgumentException("Price cannot be negative.");
        }
    }

    // Additional methods can be defined
    public int discountedPrice(int percent) {
        return price - (price * percent / 100);
    }

    public String displayInfo() {
        return String.format("[%s] %s - %,d won", category, name, price);
    }

    public static void main(String[] args) {
        Product laptop = new Product("MacBook Pro", 2500000, "Electronics");
        Product phone = new Product("Galaxy S24", 1200000, "Electronics");

        // Getters use the field name directly (not getXxx)
        System.out.println("Name: " + laptop.name());
        System.out.println("Price: " + laptop.price());
        System.out.println(laptop.displayInfo());
        System.out.println("10% discount: " + laptop.discountedPrice(10) + " won");

        // Auto-generated toString
        System.out.println(phone);

        // Auto-generated equals
        Product another = new Product("MacBook Pro", 2500000, "Electronics");
        System.out.println(laptop.equals(another)); // true
    }
}

Today’s Exercises

  1. Encapsulated Bank Account: Redesign a BankAccount class following encapsulation principles. The balance should not be directly accessible; deposits and withdrawals should only be possible through methods, and throw an exception if the balance is insufficient during withdrawal.

  2. Student Grade Management: Create a StudentGrade class. Manage the name and per-subject scores (array) as private, and provide methods to calculate the average, and retrieve the highest and lowest scores. Only allow scores in the 0-100 range.

  3. Using Records: Create an Address record that stores postal code, city/state, district, and detailed address, and returns the full address as a string via a fullAddress() method. Add postal code format validation in the compact constructor.

Was this article helpful?